/* eslint-disable no-await-in-loop */
/**
 * 文件缺陷模型
 *
 * Created by yangyunze on 2021/03/08.
 */
const mongoose = require('mongoose');
const path = require('path');
const Rule = require('./mongo/rule');
const ProjectVersion = require('./mongo/project-version');
const Defect = require('./mongo/file-defect');
const RulesStat = require('./mongo/rule-stat');
const Project = require('./mongo/project');
const File = require('./mongo/file');
const PhoenixError = require('./phoenix-error');
const ruleModel = require('./rules');
const templates = require('./analyze-templates');
const publisher = require('./redis/publisher');
const common = require('./common');
const utility = require('./utility');
const fileModels = require('./files');
const numbers = require('./numbers');
const { eventHub, eventKind } = require('./event-hub');
const defectStatus = {
  toBeEdit: 0, // 待修改
  edited: 1, // 已修改
  ignore: 2, // 忽略
  misreport: 3, // 误报
  explained: 4, // 已说明
};
const defectType = {
  original: 0, // 原始缺陷
  add: 1, // 新增缺陷

};

async function clearAllRuleStatistics(versionId) {
  return RulesStat.deleteMany({ versionId: mongoose.Types.ObjectId(versionId) });
}

async function updateRuleStatistics(versionId, fileIds) {
  const match = {
    defectType: { $in: [defectType.add, undefined] },
  };
  if (fileIds) {
    match.fileId = { $in: fileIds };
  } else {
    match.versionId = mongoose.Types.ObjectId(versionId);
  }
  const defectGroup = {
    _id: { fileId: '$fileId', ruleId: '$ruleId' },
    total: { $sum: 1 },
    filePath: { $first: '$filePath' },
    ruleSetId: { $first: '$ruleSetId' },
  };
  Object.keys(defectStatus).forEach((statusKey) => {
    defectGroup[`total_${statusKey}`] = {
      $sum: {
        $cond: {
          if: { $eq: ['$status', defectStatus[statusKey]] },
          then: 1,
          else: 0,
        },
      },
    };
  });
  let ruleStatistics = await Defect.aggregate([
    {
      $match: match,
    },
    {
      $group: defectGroup,
    },
  ]).allowDiskUse(true);
  if (ruleStatistics.length === 0) {
    return;
  }
  ruleStatistics = ruleStatistics.map((stat) => ({
    versionId,
    ruleSetId: stat.ruleSetId,
    ruleId: stat._id.ruleId,
    fileId: stat._id.fileId,
    filePath: stat.filePath,
    total: stat.total,
    total_toBeEdit: stat.total_toBeEdit,
    total_edited: stat.total_edited,
    total_ignore: stat.total_ignore,
    total_misreport: stat.total_misreport,
    total_explained: stat.total_explained,
  }));
  await RulesStat.collection.insertMany(ruleStatistics, { ordered: false });
}

async function getRulesStatistics(versionId, { extensions, status, fileIds }, groupBy = 'rule') {
  versionId = mongoose.Types.ObjectId(versionId);
  const hasCache = await RulesStat.findOne({ versionId }).lean();
  // 原始逻辑
  if (!hasCache) {
    const match = {
      versionId,
      defectType: { $in: [defectType.add, undefined] },
    };
    if (status) {
      match.status = { $in: [].concat(status).map((status) => Number.parseInt(status, 10)) };
    }
    if (extensions) {
      extensions = [].concat(extensions);
      match.filePath = RegExp(`.*(${extensions.map(utility.regExpQuote).join('|')})$`);
    }
    if (fileIds) {
      match.fileId = { $in: fileIds.map((id) => mongoose.Types.ObjectId(id)) };
    }
    const defectGroup = {
      total: { $sum: 1 },
    };
    Object.keys(defectStatus).forEach((statusKey) => {
      defectGroup[`total_${statusKey}`] = {
        $sum: {
          $cond: {
            if: { $eq: ['$status', defectStatus[statusKey]] },
            then: 1,
            else: 0,
          },
        },
      };
    });
    if (groupBy === 'rule') {
      defectGroup._id = '$ruleId';
      return Defect.aggregate([
        {
          $match: match,
        },
        {
          $group: defectGroup,
        },
      ]);
    }
    defectGroup._id = { fileId: '$fileId', ruleId: '$ruleId' };
    defectGroup.fileId = { $first: '$fileId' };
    defectGroup.ruleId = { $first: '$ruleId' };
    defectGroup.level = { $first: '$level' };
    defectGroup.originalLevel = { $first: '$originalLevel' };
    return Defect.aggregate([
      {
        $match: match,
      },
      {
        $group: defectGroup,
      },
      {
        $unset: '_id',
      },
    ]).allowDiskUse(true);
  }
  const match = {
    versionId,
  };
  if (extensions) {
    extensions = [].concat(extensions);
    match.filePath = RegExp(`.*(${extensions.map(utility.regExpQuote).join('|')})$`);
  }
  if (status) {
    const statusReverseMap = Object.fromEntries(
      Object.entries(defectStatus).map(([key, value]) => [value, key])
    );
    match.$or = [];
    status.forEach((s) => {
      match.$or.push({ [`total_${statusReverseMap[s]}`]: { $gt: 0 } });
    });
  }
  if (fileIds) {
    match.fileId = { $in: fileIds.map((id) => mongoose.Types.ObjectId(id)) };
  }
  let result;
  if (groupBy === 'rule') {
    const pipeline = [{ $match: match }];
    pipeline.push({
      $group: {
        _id: '$ruleId',
        ruleSetId: { $first: '$ruleSetId' },
        total: { $sum: '$total' },
        total_toBeEdit: { $sum: '$total_toBeEdit' },
        total_edited: { $sum: '$total_edited' },
        total_ignore: { $sum: '$total_ignore' },
        total_misreport: { $sum: '$total_misreport' },
        total_explained: { $sum: '$total_explained' },
      },
    });
    result = await RulesStat.aggregate(pipeline);
  } else {
    result = await RulesStat.find(match).lean();
  }
  return result;
}

async function getDefectTree(versionId, {
  levels,
  ruleSets,
  defectStatus: status,
  extensions,
} = {}, rulesProject = {}) {
  const vid = mongoose.Types.ObjectId(versionId);
  const version = await ProjectVersion.findById(vid);
  if (!version) {
    throw new PhoenixError('not found', `project version ${versionId} not exist`);
  }
  const templateId = version.analyzeTemplate;
  if (!templateId) {
    return {
      ruleSets: [],
      total: 0,
    };
    // throw new PhoenixError('not found', `project version ${versionId} do not have template`);
  }
  // 许可证控制模板规则显示
  let ruleSetIds = await ruleModel.filterRuleSetByLicense();
  const template = await templates.getTemplate(templateId);
  if (!template) {
    throw new PhoenixError('not found', `template ${templateId} not exist`);
  }
  if (ruleSets) {
    ruleSets = [].concat(ruleSets);
    ruleSetIds = ruleSetIds.filter((ruleSetId) => ruleSets.indexOf(ruleSetId.toString()) >= 0);
  }
  const rules = await Rule.aggregate([
    {
      $match: {
        _id: { $in: template.rules },
        ruleSetId: { $in: ruleSetIds },
      },
    },
    {
      $lookup: {
        from: 'rule-sets', localField: 'ruleSetId', foreignField: '_id', as: 'ruleSet',
      },
    },
    {
      $unwind: '$ruleSet',
    },
    {
      $project: {
        ruleSetId: '$ruleSet._id',
        ruleSetName: '$ruleSet.name',
        ruleCode: '$ruleCode',
        originalLevel: '$originalLevel',
        level: 1,
        ruleId: '$_id',
        name: 1,
        orderIndex: 1,
        ...rulesProject,
      },
    },
  ]);
  const ruleDefectCount = await getRulesStatistics(versionId, { status, extensions });
  const ruleDefectCountMap = new Map(
    ruleDefectCount.map((item) => {
      const totalByStatus = Object.keys(defectStatus).reduce((obj, key) => {
        obj[key] = item[`total_${key}`];
        return obj;
      }, {});
      return [item._id.toString(), { total: item.total, totalByStatus }];
    })
  );
  rules.forEach((r) => {
    const defectInfo = ruleDefectCountMap.get(r._id.toHexString());
    r.total = defectInfo && defectInfo.total ? defectInfo.total : 0;
    r.totalByStatus = defectInfo ? defectInfo.totalByStatus : {};
  });

  // 树构建
  const sets = rules.reduce((ruleSets, rule) => {
    const { ruleSetId, ruleSetName } = rule;
    let set = ruleSets.find((rs) => rs.ruleSetId.toHexString() === ruleSetId.toHexString());
    if (!set) {
      set = {
        ruleSetId,
        ruleSetName,
        rules: [],
        total: 0,
      };
      ruleSets.push(set);
    }
    set.rules.push(rule);
    return ruleSets;
  }, []);
  if (levels) {
    levels = [].concat(levels).map((level) => Number.parseInt(level, 10));
  }
  const levelsSet = levels ? new Set(levels) : undefined;
  sets.forEach((set) => {
    set.levels = set.rules.reduce((levels, rule) => {
      ruleModel.formatRulesForUserDefined(rule, template.userDefined);
      const {
        level, total, totalByStatus, _id: ruleId, name, ruleCode, orderIndex, originalLevel, subLevelOrder,
      } = rule;
      if (levelsSet && !levelsSet.has(level)) {
        return levels;
      }
      let levelObj = levels.find((l) => l.level === level);
      if (!levelObj) {
        levelObj = {
          level,
          total: 0,
          totalByStatus: {},
          rules: [],
        };
        levels.push(levelObj);
      }
      if (total) {
        // 只展示有缺陷的rule
        const ruleData = {
          ruleId, name, total, totalByStatus, ruleCode, orderIndex, originalLevel, subLevelOrder,
        };
        Object.keys(rulesProject).forEach((key) => {
          ruleData[key] = rule[key];
        });
        levelObj.rules.push(ruleData);
        levelObj.total += total;
        utility.mergeAdd(levelObj.totalByStatus, totalByStatus);
      }
      return levels;
    }, []);
    delete set.rules;
    let setTotal = 0;
    const setTotalByStatus = {};
    set.levels
      .sort((l1, l2) => (l1.level > l2.level ? 1 : -1))
      .forEach((level) => {
        setTotal += level.total;
        utility.mergeAdd(setTotalByStatus, level.totalByStatus);
        level.rules.sort((a, b) => {
          if (a.subLevelOrder === b.subLevelOrder) {
            return a.orderIndex > b.orderIndex ? 1 : -1;
          }
          return a.subLevelOrder > b.subLevelOrder ? 1 : -1;
        });
      });
    set.total = setTotal;
    set.totalByStatus = setTotalByStatus;
  });
  return {
    ruleSets: sets
      .sort((s1, s2) => (s1.ruleSetName > s2.ruleSetName ? 1 : -1)),
    total: sets.reduce((cnt, set) => cnt + set.total, 0),
    totalByStatus: sets.reduce((obj, set) => {
      utility.mergeAdd(obj, set.totalByStatus);
      return obj;
    }, {}),
  };
}

async function getDefects(versionId, {
  filePath,
  fileId,
  ruleId,
  levels,
  status,
  ruleSets,
  includeOriginal = false,
  extensions,
} = {}) {
  const match = {
    versionId: mongoose.Types.ObjectId(versionId),
  };
  if (!includeOriginal) {
    match.defectType = { $in: [defectType.add, undefined] };
  }
  if (fileId) {
    match.fileId = mongoose.Types.ObjectId(fileId);
  }
  if (ruleId) {
    match.ruleId = mongoose.Types.ObjectId(ruleId);
  }
  if (ruleSets) {
    match.ruleSetId = { $in: [].concat(ruleSets).map((id) => mongoose.Types.ObjectId(id)) };
  }
  if (status) {
    match.status = { $in: [].concat(status).map((s) => Number.parseInt(s, 10)) };
  }
  if (levels) {
    levels = [].concat(levels).map((level) => Number.parseInt(level, 10));
  }
  if (filePath) {
    match.filePath = filePath;
  } else if (extensions) {
    match.filePath = RegExp(`.*(${extensions.map(utility.regExpQuote).join('|')})$`);
  }
  let defects = await Defect.aggregate([
    {
      $match: match,
    },
    {
      $lookup: {
        from: 'rules', localField: 'ruleId', foreignField: '_id', as: 'rule',
      },
    },
    {
      $unwind: '$rule',
    },
    {
      $lookup: {
        from: 'rule-sets', localField: 'rule.ruleSetId', foreignField: '_id', as: 'ruleSet',
      },
    },
    {
      $unwind: '$ruleSet',
    },
    {
      $project: {
        defectId: '$_id',
        _id: 0,
        fileId: 1,
        projectId: 1,
        versionId: 1,
        filePath: 1,
        number: '$defectNumber',
        message: '$message',
        locations: '$locations',
        level: '$level',
        originalLevel: '$originalLevel',
        ruleId: 1,
        ruleName: '$rule.name',
        ruleCode: '$rule.ruleCode',
        ruleLevel: '$rule.level',
        ruleOriginalLevel: '$rule.originalLevel',
        ruleSetId: '$ruleSet._id',
        ruleSetName: '$ruleSet.name',
        ruleOrderIndex: '$rule.orderIndex',
        status: 1,
        steps: 1,
        description: 1,
        notes: 1,
      },
    },
    {
      $sort: {
        ruleOrderIndex: 1,
        filePath: 1,
        'locations.begin.line': 1,
        'locations.begin.column': 1,
      },
    },
  ]).allowDiskUse(true);
  // 兼容老的数据没有level字段
  let template = null;
  if (defects.length > 0 && !defects[0].level) {
    const version = await ProjectVersion.findById(mongoose.Types.ObjectId(versionId));
    template = await templates.getTemplate(version.analyzeTemplate);
  }
  defects = defects.map((defect) => {
    if (template) {
      defect.level = defect.ruleLevel;
      defect.originalLevel = defect.ruleOriginalLevel;
    }
    ruleModel.formatRulesForUserDefined(defect, template ? template.userDefined : null);
    return {
      ...defect,
      levelName: ruleModel.ruleLevelEum[defect.originalLevel].name,
      fileName: path.basename(defect.filePath),
    };
  });
  if (utility.isArrayType(levels) && levels.length) {
    defects = defects.filter((defect) => levels.indexOf(defect.level) >= 0);
  }
  return defects;
}

async function getFilesRuleDefectsCount(fileIds, {
  levels,
  versionId,
} = {}) {
  if (!utility.isArrayType(fileIds)) {
    fileIds = [fileIds];
  }
  if (levels) {
    levels = [].concat(levels).map((level) => Number.parseInt(level, 10));
  }
  let defects = await getRulesStatistics(versionId, { fileIds }, 'file');
  const ruleIds = defects.reduce((set, def) => {
    set.add(def.ruleId.toString());
    return set;
  }, new Set());
  const rules = await Rule.aggregate([
    {
      $match: {
        _id: { $in: [...ruleIds].map(mongoose.Types.ObjectId) },
      },
    },
    {
      $lookup: {
        from: 'rule-sets', localField: 'ruleSetId', foreignField: '_id', as: 'ruleSet',
      },
    },
    {
      $unwind: '$ruleSet',
    },
    {
      $project: {
        name: 1,
        ruleCode: 1,
        level: 1,
        originalLevel: 1,
        orderIndex: 1,
        'ruleSet.name': 1,
      },
    },
  ]);
  const rulesMap = rules.reduce((map, rule) => {
    map.set(rule._id.toString(), rule);
    return map;
  }, new Map());
  // 兼容老的数据没有level字段
  let template = null;
  if (defects.length > 0 && !defects[0].level) {
    const version = await ProjectVersion.findById(mongoose.Types.ObjectId(versionId));
    template = await templates.getTemplate(version.analyzeTemplate);
  }
  const needFilter = utility.isArrayType(levels) && levels.length;

  defects = defects.filter((defect) => {
    const rule = rulesMap.get(defect.ruleId.toString());
    if (rule) {
      defect.ruleName = rule.name;
      defect.ruleCode = rule.ruleCode;
      defect.ruleLevel = rule.level;
      defect.ruleOriginalLevel = rule.originalLevel;
      defect.orderIndex = rule.orderIndex;
      defect.ruleSetName = rule.ruleSet.name;
      defect.totalByStatus = Object.keys(defectStatus).reduce((obj, key) => {
        obj[key] = defect[`total_${key}`];
        delete defect[`total_${key}`];
        return obj;
      }, {});
    }
    if (template) {
      defect.level = defect.ruleLevel;
      defect.originalLevel = defect.ruleOriginalLevel;
    }
    ruleModel.formatRulesForUserDefined(defect, template ? template.userDefined : null);
    if (needFilter && (levels.indexOf(defect.level) < 0)) {
      return false;
    }
    return true;
  });

  defects.sort((a, b) => {
    if (a.ruleSetName !== b.ruleSetName) {
      return a.ruleSetName > b.ruleSetName ? 1 : -1;
    }
    if (a.subLevelOrder !== b.subLevelOrder) {
      return a.subLevelOrder > b.subLevelOrder ? 1 : -1;
    }
    return a.orderIndex > b.orderIndex ? 1 : -1;
  });

  return defects;
}

async function updateDefect(defectIds, doc) {
  const dids = defectIds.map((id) => mongoose.Types.ObjectId(id));
  const result = await Defect.updateMany({ _id: { $in: dids } }, {
    $set: doc,
  });
  if (!result.ok) {
    throw new PhoenixError('not found', 'defects update failed');
  }
  if (doc.status) {
    const defects = await Defect.aggregate([
      { $match: { _id: { $in: dids } } },
      {
        $group: {
          _id: '$fileId',
          versionId: { $first: '$versionId' },
        },
      },
    ]);
    if (!defects.length) {
      return;
    }
    const fileIds = defects.map((d) => d._id);
    await RulesStat.deleteMany({ fileId: { $in: fileIds } });
    await updateRuleStatistics(defects[0].versionId, fileIds);
  }
  // return Object.assign(exists.toObject(), doc);
}

async function getVersionOverview(_versionId, { ruleSets, defectStatus, levels } = {}) {
  const vid = mongoose.Types.ObjectId(_versionId);
  const [projectVersion] = await ProjectVersion.aggregate([
    {
      $match: { _id: vid },
    },
    {
      $lookup: {
        from: 'projects', localField: 'projectId', foreignField: '_id', as: 'project',
      },
    },
    {
      $unwind: { path: '$project', preserveNullAndEmptyArrays: true },
    },
    {
      $lookup: {
        from: 'files', localField: 'rootDirectoryId', foreignField: '_id', as: 'rootDirectory',
      },
    },
    {
      $unwind: { path: '$rootDirectory', preserveNullAndEmptyArrays: true },
    },
  ]);
  if (!projectVersion) {
    throw new PhoenixError('not found', `project version ${_versionId} not exists`);
  }

  const match = { versionId: vid, defectType: { $in: [defectType.add, undefined] } };
  if (ruleSets) {
    match.ruleSetId = { $in: [].concat(ruleSets).map((id) => mongoose.Types.ObjectId(id)) };
  }
  if (defectStatus) {
    match.status = { $in: [].concat(defectStatus).map((status) => Number.parseInt(status, 10)) };
  }
  if (levels) {
    match.level = { $in: [].concat(levels).map((level) => Number.parseInt(level, 10)) };
  }

  const filePromise = Defect.aggregate([
    {
      $match: match,
    },
    {
      $group: {
        _id: '$fileId',
        total: { $sum: 1 },
      },
    },
    {
      $sort: {
        total: -1,
        _id: 1,
      },
    },
    {
      $limit: 10,
    },
    {
      $lookup: {
        from: 'files', localField: '_id', foreignField: '_id', as: 'file',
      },
    },
    {
      $unwind: '$file',
    },
    {
      $project: {
        _id: 0,
        fileId: '$_id',
        filePath: '$file.path',
        fileName: '$file.fileName',
        total: 1,
      },
    },
  ]);
  const rulePromise = Defect.aggregate([
    {
      $match: match,
    },
    {
      $group: {
        _id: '$ruleId',
        total: { $sum: 1 },
      },
    },
    {
      $sort: {
        total: -1,
        _id: 1,
      },
    },
    {
      $limit: 10,
    },
    {
      $lookup: {
        from: 'rules', localField: '_id', foreignField: '_id', as: 'rule',
      },
    },
    {
      $unwind: '$rule',
    },
    {
      $lookup: {
        from: 'rule-sets', localField: 'rule.ruleSetId', foreignField: '_id', as: 'ruleSet',
      },
    },
    {
      $unwind: '$ruleSet',
    },
    {
      $project: {
        _id: 0,
        ruleId: '$_id',
        name: '$rule.name',
        ruleCode: '$rule.ruleCode',
        ruleSet: '$ruleSet.name',
        total: 1,
      },
    },
  ]);

  const [files, rules] = await Promise.all([filePromise, rulePromise]);

  const sourceExts = Object.values(projectVersion.languages)
    .reduce((arr, item) => arr.concat(item.suffixes), []);
  const headerExts = projectVersion.headers.suffixes;
  // 代码行数统计
  const [filesInfo] = await File.aggregate([
    {
      $match: {
        versionId: vid,
        extname: { $in: [...sourceExts, ...headerExts] },
      },
    },
    {
      $group: {
        _id: null,
        numberOfLines: { $sum: '$numberOfLines' },
      },
    },
  ]);
  const { numberOfLines } = filesInfo || { numberOfLines: 0 };

  // 文件数统计
  const numberOfFiles = await File.countDocuments({
    versionId: vid,
    extname: { $in: [...sourceExts, ...headerExts] },
  }).countDocuments();

  let defectsCount = {};
  const keyLevels = {
    mandatory: 1,
    required: 2,
    advisory: 3,
    designTaboo: 7,
  };
  if (!ruleSets && !defectStatus && !levels) {
    defectsCount = projectVersion.rootDirectory ? projectVersion.rootDirectory.defectsCount : null;
  } else {
    (await Defect.aggregate([
      {
        $match: match,
      },
      {
        $group: {
          _id: '$level',
          total: { $sum: 1 },
        },
      },
    ])).forEach((cnt) => {
      Object.keys(keyLevels).forEach((key) => {
        if (keyLevels[key] === cnt._id) {
          defectsCount[key] = cnt.total;
        }
      });
    });
  }
  const levelStatistic = Object.keys(keyLevels).reduce((arr, key) => {
    defectsCount = defectsCount || {};
    const total = defectsCount[key] || 0;
    const kloc = Math.ceil((total / numberOfLines) * 1000);
    arr.push({
      level: keyLevels[key],
      total,
      kloc,
    });
    return arr;
  }, []);
  const {
    versionName, _id: versionId, project,
  } = projectVersion;
  return {
    versionName,
    versionId,
    projectId: project._id,
    projectName: project.projectName,
    levels: levelStatistic,
    files,
    rules,
    numberOfFiles,
    numberOfLines,
  };
}

async function getAllVersionsOverview({
  projectName = undefined,
  versionName = undefined,
  lastProjectOnly = false, // 只显示用户当前项目
  lastVersionOnly = false, // 只显示项目的当前版本
} = {}) {
  const projectExec = Project.aggregate();
  if (lastProjectOnly) {
    projectExec.append({
      $group: {
        _id: '$creatorId',
        projects: {
          $push: {
            creatorId: '$creatorId',
            _id: '$_id',
            projectName: '$projectName',
            lastVersionId: '$lastVersionId',
            visitAt: '$visitAt',
          },
        },
      },
    }, {
      $replaceRoot: {
        newRoot: {
          // 只取最后访问项目
          $reduce: {
            input: '$projects',
            initialValue: null,
            in: {
              $cond: {
                if: { $gt: ['$$this.visitAt', '$$value.visitAt'] },
                then: '$$this',
                else: '$$value',
              },
            },
          },
        },
      },
    });
  }
  if (projectName) {
    projectExec.append({ $match: { projectName } });
  } else {
    projectExec.append({ $match: { projectName: { $ne: '' } } });
  }
  if (lastVersionOnly) {
    // 取最新版本
    projectExec.append({
      $lookup: {
        from: 'projectversions', localField: 'lastVersionId', foreignField: '_id', as: 'version',
      },
    }, {
      $unwind: '$version',
    });
  } else {
    // 取所有版本
    projectExec.append({
      $lookup: {
        from: 'projectversions', localField: '_id', foreignField: 'projectId', as: 'version',
      },
    }, {
      $unwind: '$version',
    });
  }
  if (versionName !== undefined) {
    projectExec.append({ $match: { 'version.versionName': versionName } });
  }
  projectExec.append(
    {
      $match: {
        'version.masterVersionId': null, // 只取master version
      },
    },
    {
      $lookup: {
        from: 'projectversions', localField: 'version._id', foreignField: 'masterVersionId', as: 'subVersion',
      },
    },
    {
      $unwind: '$subVersion',
    },
    {
      $match: {
        'subVersion.versionType': 'staticAnalyze',
      },
    }
  );
  projectExec.append({
    $replaceRoot: {
      newRoot: '$subVersion',
    },
  });
  projectExec.append({
    $lookup: {
      from: 'files', localField: 'rootDirectoryId', foreignField: '_id', as: 'rootDirectory',
    },
  }, {
    $unwind: { path: '$rootDirectory', preserveNullAndEmptyArrays: true },
  }, {
    $set: {
      __extNames: {
        $concatArrays: [{
          $reduce: {
            input: { $objectToArray: '$languages' },
            initialValue: [],
            in: { $concatArrays: ['$$value', '$$this.v.suffixes'] },
          },
        }, '$headers.suffixes'],
      },
    },
  }, {
    $group: {
      _id: null,
      versions: {
        $push: {
          versionId: '$_id',
          extNames: '$__extNames',
        },
      },
      defectsCountMandatory: { $sum: '$rootDirectory.defectsCount.mandatory' },
      defectsCountRequired: { $sum: '$rootDirectory.defectsCount.required' },
      defectsCountAdvisory: { $sum: '$rootDirectory.defectsCount.advisory' },
      defectsCountDesignTaboo: { $sum: '$rootDirectory.defectsCount.designTaboo' },
    },
  });
  const ret = await projectExec.exec();
  if (!ret.length) {
    return {
      levels: [],
      files: [],
      rules: [],
    };
  }
  const [{
    versions, defectsCountMandatory, defectsCountRequired, defectsCountAdvisory, defectsCountDesignTaboo,
  }] = ret;
  const filePromise = Defect.aggregate([
    {
      $match: {
        versionId: { $in: versions.map((v) => v.versionId) },
        defectType: { $in: [defectType.add, undefined] },
      },
    },
    {
      $group: {
        _id: '$fileId',
        total: { $sum: 1 },
      },
    },
    {
      $sort: {
        total: -1,
        _id: 1,
      },
    },
    {
      $limit: 10,
    },
    {
      $lookup: {
        from: 'files', localField: '_id', foreignField: '_id', as: 'file',
      },
    },
    {
      $unwind: '$file',
    },
    {
      $project: {
        _id: 0,
        fileId: '$_id',
        filePath: '$file.path',
        fileName: '$file.fileName',
        total: 1,
      },
    },
  ]);
  const rulePromise = Defect.aggregate([
    {
      $match: {
        versionId: { $in: versions.map((v) => v.versionId) },
        defectType: { $in: [defectType.add, undefined] },
      },
    },
    {
      $group: {
        _id: '$ruleId',
        total: { $sum: 1 },
      },
    },
    {
      $sort: {
        total: -1,
        _id: 1,
      },
    },
    {
      $limit: 10,
    },
    {
      $lookup: {
        from: 'rules', localField: '_id', foreignField: '_id', as: 'rule',
      },
    },
    {
      $unwind: '$rule',
    },
    {
      $project: {
        _id: 0,
        ruleId: '$_id',
        name: '$rule.name',
        total: 1,
      },
    },
  ]);

  const [files, rules] = await Promise.all([filePromise, rulePromise]);

  // 代码行数统计
  const [filesInfo] = await File.aggregate([
    {
      $match: {
        $or: versions.map(({ versionId, extNames }) => ({
          versionId,
          extname: { $in: extNames },
        })),
      },
    },
    {
      $group: {
        _id: null,
        numberOfLines: { $sum: '$numberOfLines' },
      },
    },
  ]);
  const { numberOfLines } = filesInfo || { numberOfLines: 0 };
  const defectsCount = {
    mandatory: defectsCountMandatory,
    required: defectsCountRequired,
    advisory: defectsCountAdvisory,
    designTaboo: defectsCountDesignTaboo,
  };
  const keyLevels = {
    mandatory: 1,
    required: 2,
    advisory: 3,
    designTaboo: 7,
  };
  const levels = Object.keys(keyLevels).reduce((arr, key) => {
    const total = defectsCount[key] || 0;
    const kloc = Math.ceil((total / numberOfLines) * 1000);
    arr.push({
      level: keyLevels[key],
      total,
      kloc,
    });
    return arr;
  }, []);
  return {
    levels,
    files,
    rules,
  };
}

/**
 * 删除文件下的缺陷
 * @param {String} fileId 文件Id
 * @return {Object}
 */
function removeDefectsByFileId(fileId, ruleSetIds = undefined) {
  if (ruleSetIds) {
    const filter = {
      fileId: mongoose.Types.ObjectId(fileId),
      ruleSetId: { $in: ruleSetIds.map((ruleSetId) => mongoose.Types.ObjectId(ruleSetId)) },
    };
    return Promise.all([
      Defect.deleteMany(filter),
      RulesStat.deleteMany(filter),
    ]);
  }
  const filter = { fileId: mongoose.Types.ObjectId(fileId) };
  return Promise.all([
    Defect.deleteMany(filter),
    RulesStat.deleteMany(filter),
  ]);
}

/**
 * 批量插入文件缺陷
 * @param {Array<FileDefect>>} fileDefects 规则Id数组
 * @param {object} file 文件
 * @param {Array<FileDefect>>} 任务类型
 * @return {Object}
 */
function bulkInsert(fileDefects, file) {
  const ruleIds = [];
  let defectsCount = {
    mandatory: 0,
    required: 0,
    advisory: 0,
    designTaboo: 0,
  };
  if (file && file.defectsCount) {
    defectsCount = {
      ...defectsCount,
      ...file.defectsCount,
    };
  }
  if (fileDefects.length === 0) {
    return null;
  }
  return Defect.collection.insertMany(fileDefects, { ordered: false })
    .then(() => {
      const fileDefectsCountMap = new Map();
      const fileDefectMessage = new Map();
      fileDefects.forEach((fileDefect) => {
        const ruleId = fileDefect.ruleId.toString();
        ruleIds.push(ruleId);
        const fileId = fileDefect.fileId.toString();
        if (!fileDefectsCountMap.has(fileId)) {
          fileDefectsCountMap.set(
            fileId,
            {
              ...defectsCount,
            }
          );
        }

        if (fileDefect.defectType === defectType.add) {
          switch (fileDefect.level) {
            case ruleModel.ruleLevel.mandatory:
              fileDefectsCountMap.get(fileId).mandatory += 1;
              break;
            case ruleModel.ruleLevel.required:
              fileDefectsCountMap.get(fileId).required += 1;
              break;
            case ruleModel.ruleLevel.advisory:
              fileDefectsCountMap.get(fileId).advisory += 1;
              break;
            case ruleModel.ruleLevel.designTaboo:
              fileDefectsCountMap.get(fileId).designTaboo += 1;
              break;
            default:
              break;
          }
          // prepare websocket message
          if (!fileDefectMessage.has(ruleId)) {
            fileDefectMessage.set(
              ruleId,
              {
                versionId: fileDefect.versionId.toString(),
                fileId,
                ruleId,
                ruleSetId: fileDefect.ruleSetId.toString(),
                level: fileDefect.level,
                subLevelOrder: fileDefect.subLevelOrder,
                count: 0,
              }
            );
          }
          fileDefectMessage.get(ruleId).count += 1;
        }
      });
      return Rule.find(
        {
          _id: { $in: utility.arrayDeduplicate(ruleIds).map((id) => mongoose.Types.ObjectId(id)) },
        },
        ['name', 'orderIndex', 'ruleCode']
      )
        .then(async (rules) => {
          const rulesDetailMap = new Map();
          rules.forEach((rule) => {
            rulesDetailMap.set(
              rule._id.toString(), { name: rule.name, orderIndex: rule.orderIndex, ruleCode: rule.ruleCode }
            );
          });
          await RulesStat.collection.insertMany([...fileDefectMessage.values()].map((item) => ({
            versionId: file.versionId,
            ruleSetId: mongoose.Types.ObjectId(item.ruleSetId),
            ruleId: mongoose.Types.ObjectId(item.ruleId),
            fileId: file._id,
            filePath: file.path,
            total: item.count,
            total_toBeEdit: item.count,
            total_edited: 0,
            total_ignore: 0,
            total_misreport: 0,
            total_explained: 0,
          })), { ordered: false });
          const fileDefectOfRules = await RulesStat.aggregate([
            {
              $match: {
                fileId: file._id,
              },
            },
            {
              $lookup: {
                from: 'rule-sets', localField: 'ruleSetId', foreignField: '_id', as: 'ruleSet',
              },
            },
            {
              $unwind: '$ruleSet',
            },
            {
              $group: {
                _id: '$ruleId',
                count: { $sum: '$total' },
                ruleSetName: { $first: '$ruleSet.name' },
              },
            },
          ]);
          const ruleStatsMap = new Map();
          fileDefectOfRules.forEach((stat) => {
            ruleStatsMap.set(stat._id.toString(), {
              count: stat.count,
              ruleSetName: stat.ruleSetName,
            });
          });
          fileDefectMessage.forEach((fileDefect) => {
            const { name, orderIndex, ruleCode } = rulesDetailMap.get(fileDefect.ruleId.toString());
            fileDefect.name = name;
            fileDefect.orderIndex = orderIndex;
            fileDefect.ruleCode = ruleCode;
            fileDefect.total = 0;
            fileDefect.ruleSetName = '';
            const ruleId = fileDefect.ruleId.toString();
            if (ruleStatsMap.has(ruleId)) {
              fileDefect.total = ruleStatsMap.get(ruleId).count;
              fileDefect.ruleSetName = ruleStatsMap.get(ruleId).ruleSetName;
            }
            publisher.publishFileDefectMessage('add', fileDefect);
          });
        })
        .then(() => {
          const promise = [];
          fileDefectsCountMap.forEach((count, fileId) => {
            promise.push(
              File.findOne({ _id: mongoose.Types.ObjectId(fileId) })
                .then((file) => {
                  const incDoc = { $inc: {} };
                  ['mandatory', 'required', 'advisory', 'designTaboo'].forEach((key) => {
                    incDoc.$inc[`defectsCount.${key}`] = count[key]
                      - (!file.defectsCount ? 0 : (file.defectsCount[key] || 0));
                  });
                  if (incDoc.$inc['defectsCount.mandatory'] === 0
                    && incDoc.$inc['defectsCount.required'] === 0
                    && incDoc.$inc['defectsCount.advisory'] === 0
                    && incDoc.$inc['defectsCount.designTaboo'] === 0) {
                    return Promise.resolve();
                  }
                  return common.increaseDirectories(fileId, incDoc);
                })
            );
          });
          return Promise.all(promise);
        });
    });
}

/**
 * 清空静态分析结果
 * @param {String} versionId 版本Id
 * @return {Object}
 */
async function clearDefects(versionId) {
  versionId = mongoose.Types.ObjectId(versionId);
  const ruleIds = (await Defect.find({ versionId })).map((rule) => `${versionId.toString()}:${rule.ruleId.toString()}-defect`);
  await Promise.all(
    [
      numbers.clearDefectNumber(ruleIds),
      Defect.deleteMany({ versionId }),
      RulesStat.deleteMany({ versionId }),
      File.updateMany({ versionId }, { $unset: { defectsCount: 1, analyzeTaskId: 1 } }),
    ]
  );
  publisher.publishFileDefectMessage('clear-all-defects', { versionId });
}

async function clearFilesDefects(versionId, fileId, files = []) {
  versionId = mongoose.Types.ObjectId(versionId);
  if (files.length === 0 && fileId) {
    fileId = mongoose.Types.ObjectId(fileId);
    const file = await File.findOne({ _id: fileId }, { path: 1, kind: 1 });
    let matchStr = file.path;
    if (file.kind === fileModels.kind.directory) {
      matchStr = `${file.path}/`;
    }
    files = await File.find({
      versionId,
      path: new RegExp(`^${utility.regExpQuote(matchStr)}`),
      kind: fileModels.kind.file,
    }, { _id: 1, directoryId: 1 });
  }
  await utility.arrayChunkOperation(files, 100, async (file) => {
    await Promise.all([
      Defect.deleteMany({ fileId: file._id }),
      RulesStat.deleteMany({ fileId: file._id }),
    ]);
    file = await File.findOneAndUpdate({ _id: file._id }, { $unset: { defectsCount: 1 } });
    if (!file.defectsCount) {
      return;
    }
    const incDoc = { $inc: {} };
    const defectLevels = Object.keys(file.defectsCount);
    defectLevels.forEach((defectLevelName) => {
      incDoc.$inc[`defectsCount.${defectLevelName}`] = -file.defectsCount[defectLevelName];
    });
    await common.increaseDirectories(file.directoryId, incDoc);
    publisher.publishFileDefectMessage('clear-file-defects', { versionId, fileId: file._id });
  });
}

async function clearDefectsOfTemplate(templateId) {
  const tid = mongoose.Types.ObjectId(templateId);
  const versions = await ProjectVersion.find({
    analyzeTemplate: tid,
  }, { _id: 1 });
  await Promise.all(versions.map((v) => clearDefects(v._id)));
}

async function countByLevelAndStatus(versionId, { ruleSets, defectStatus, levels } = {}) {
  versionId = mongoose.Types.ObjectId(versionId);
  const match = {
    versionId,
    defectType: { $in: [defectType.add, undefined] },
  };
  if (ruleSets) {
    match.ruleSetId = { $in: [].concat(ruleSets).map((id) => mongoose.Types.ObjectId(id)) };
  }
  if (defectStatus) {
    defectStatus = !utility.isArrayType(defectStatus) ? [defectStatus] : defectStatus;
    match.status = { $in: [].concat(defectStatus).map((status) => Number.parseInt(status, 10)) };
  }
  if (levels) {
    levels = [].concat(levels).map((level) => Number.parseInt(level, 10));
  }
  const arr = await Defect.aggregate([
    {
      $match: match,
    },
    {
      $lookup: {
        from: 'rules', localField: 'ruleId', foreignField: '_id', as: 'rule',
      },
    },
    {
      $unwind: '$rule',
    },
    {
      $project: {
        level: '$level',
        originalLevel: '$originalLevel',
        ruleId: '$ruleId',
        status: '$status',
        ruleLevel: '$rule.level',
        ruleOriginalLevel: '$rule.originalLevel',
        _id: 0,
      },
    },
  ]);
  // 兼容老的数据没有level字段
  let template = null;
  if (arr.length > 0 && !arr[0].level) {
    const version = await ProjectVersion.findById(versionId);
    template = await templates.getTemplate(version.analyzeTemplate);
  }
  const groupMap = new Map();
  arr.forEach((item) => {
    if (template) {
      item.level = item.ruleLevel;
      item.originalLevel = item.ruleOriginalLevel;
    }
    ruleModel.formatRulesForUserDefined(item, template ? template.userDefined : null);
    if (levels && levels.indexOf(item.level) < 0) {
      return;
    }
    const key = `${item.level}|${item.status}`;
    if (!groupMap.has(key)) {
      groupMap.set(
        key,
        {
          level: item.level,
          status: item.status,
          total: 0,
        }
      );
    }
    groupMap.get(key).total += 1;
  });
  return [...groupMap.values()];
}
async function getDefectFileByVersionId(versionId, { ruleSets, defectStatus, levels } = {}) {
  const match = { versionId: mongoose.Types.ObjectId(versionId) };
  if (ruleSets) {
    match.ruleSetId = { $in: [].concat(ruleSets).map((id) => mongoose.Types.ObjectId(id)) };
  }
  if (defectStatus) {
    match.status = { $in: [].concat(defectStatus).map((status) => Number.parseInt(status, 10)) };
  }
  if (levels) {
    match.level = { $in: [].concat(levels).map((level) => Number.parseInt(level, 10)) };
  }
  const files = await Defect.aggregate([{
    $match: match,
  },
  {
    $group: {
      _id: '$fileId',
    },
  }, {
    $lookup: {
      from: 'files',
      localField: '_id',
      foreignField: '_id',
      as: 'defectFiles',
    },
  },
  {
    $unwind: '$defectFiles',
  },
  {
    $project: {
      fileId: '$_id',
      fileName: '$defectFiles.fileName',
      path: '$defectFiles.path',
      kind: '$defectFiles.kind',
    },
  },

  ]);
  return files;
}
async function removeDefectsByLicense(versionId) {
  const ruleSetIds = await ruleModel.filterRuleSetByLicense();
  const ruleIds = (await Defect.aggregate([
    {
      $match: {
        versionId: mongoose.Types.ObjectId(versionId),
        ruleSetId: { $nin: ruleSetIds },
      },
    },
    {
      $group: {
        _id: '$ruleId',
      },
    },
  ])).map((rule) => rule._id);
  if (ruleIds.length === 0) {
    return;
  }
  await ruleModel.updateFileDefectsCount(ruleIds, versionId);
  await Promise.all([
    Defect.deleteMany({ ruleId: { $in: ruleIds }, versionId }),
    RulesStat.deleteMany({ versionId, ruleId: { $in: ruleIds } }),
  ]);
}
/**
 *  将缺陷根据文件进行group
 * @param {ObjectId|String} versionId 版本d
 * @param {Array[ObjectId]} defectIds 缺陷id数组
 * @returns
 */
function getDefectByGroupFile(versionId,
  {
    defectStatus: status,
    extensions,
    levels,
    ruleSets,
  }) {
  const match = {
    versionId: mongoose.Types.ObjectId(versionId),
    defectType: { $in: [defectType.add, undefined] },
  };
  if (status) {
    match.status = { $in: [].concat(status).map((status) => Number.parseInt(status, 10)) };
  }
  if (extensions) {
    extensions = [].concat(extensions);
    match.filePath = RegExp(`.*(${extensions.map(utility.regExpQuote).join('|')})$`);
  }
  if (ruleSets) {
    match.ruleSetId = { $in: [].concat(ruleSets).map((id) => mongoose.Types.ObjectId(id)) };
  }
  if (levels) {
    match.level = { $in: [].concat(levels).map((level) => Number.parseInt(level, 10)) };
  }
  return Defect.aggregate([
    {
      $match: match,
    },
    {
      $group: {
        _id: { fileId: '$fileId', filePath: '$filePath' },
        defects:
          {
            $push:
            {
              ruleSetId: '$ruleSetId', defectId: '$_id', locations: '$locations', status: '$status', description: '$description', ruleId: '$ruleId',
            },
          },

      },
    },
    {
      $project: {
        _id: 0,
        fileId: '$_id.fileId',
        filePath: '$_id.filePath',
        defects: '$defects',
      },
    },
  ]).allowDiskUse(true);
}
eventHub.on(eventKind.EVENT_FILE_REMOVE, async ({ fileIds, versionId }) => {
  const filter = { versionId: mongoose.Types.ObjectId(versionId) };
  if (fileIds) {
    filter.fileId = { $in: [].concat(fileIds).map((id) => mongoose.Types.ObjectId(id)) };
  }
  return Promise.all([
    Defect.deleteMany(filter),
    RulesStat.deleteMany(filter),
  ]);
});

module.exports = {
  defectType,
  defectStatus,

  bulkInsert,
  clearAllRuleStatistics,
  clearDefects,
  clearFilesDefects,
  clearDefectsOfTemplate,

  getDefectTree,
  getDefects,
  getFilesRuleDefectsCount,
  getVersionOverview,
  getAllVersionsOverview,
  getDefectFileByVersionId,
  getDefectByGroupFile,

  removeDefectsByFileId,
  removeDefectsByLicense,
  updateDefect,
  updateRuleStatistics,

  countByLevelAndStatus,

};
exports = Object.assign(exports, module.exports);
